/**
 *
 */
package idao.engine.executor;

import idao.DaoRuntimeException;
import idao.Query;
import idao.Update;
import idao.engine.DaoUtils;
import idao.engine.parameter.SqlParameter;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;

/**
 * A SqlExecutor defines how SQL is going to be executed.
 * 
 * @author huhao
 */
public abstract class SqlExecutor
{

    public static SqlExecutor getExecutor(Method m)
    {
        if (m.isAnnotationPresent(Query.class)) {
            return new QueryExecutor(m);
        }
        if (m.isAnnotationPresent(Update.class)) {
            return new UpdateExecutor(m);
        }
        throw new DaoRuntimeException("Dao method must be annotated with either Query or Update");

    }

    private String sql;
    private List<SqlParameter> parameters = new LinkedList<SqlParameter>();

    protected SqlExecutor()
    {
        // protected constructor for subclass
    }

    protected String getSql()
    {
        return sql;
    }

    protected List<SqlParameter> getSqlParameters()
    {
        return parameters;
    }

    public Object execute(Connection c, Object[] args)
    {
        PreparedStatement ps = null;
        try {
            ps = c.prepareStatement(this.getSql());
            // apply parameters
            for (SqlParameter parameter : this.getSqlParameters()) {
                parameter.setValue(ps, args);
            }
            return this.doExecute(ps);
        } catch (SQLException e) {
            throw new DaoRuntimeException("Unexpected exception while running [" + sql + "]", e);
        } finally {
            DaoUtils.close(ps);
        }
    }

    /**
     * Execute given statement, and return the result.
     * 
     * @param ps statement to be executed
     * @return statement result
     * @throws SQLException in case of exception
     */
    protected abstract Object doExecute(PreparedStatement ps) throws SQLException;

    protected void parseSqlAndParameters(String originSql)
    {
        // normalize the sql statment
        boolean notInsideLiteral = true;
        StringBuilder sb = new StringBuilder();
        for (int i = 0, parameterIndex = 0; i < originSql.length(); i++) {
            char ch = originSql.charAt(i);
            sb.append(ch);
            if (notInsideLiteral && (ch == '?')) {
                /*
                 * this indicates the beginning of a new parameter,
                 * characters between "?<" and ">?" are parameter expression.
                 * it's type could be one of indexed, OGNL or mixed.
                 */
                String parameterExpression = this.getParameterExpression(originSql, i);
                parameters.add(SqlParameter.getParameter(++parameterIndex, parameterExpression));
                // skip parameter chars
                i += (parameterExpression.length() + 3);
            } else if (this.isDelimiter(ch)) {
                notInsideLiteral = !notInsideLiteral;
            }
        }
        sql = sb.toString();
    }

    private String getParameterExpression(String originSql, int start)
    {
        if (originSql.charAt(start + 1) != '<') {
            throw new DaoRuntimeException("Invalid parameter, must start with '?<'");
        }
        StringBuilder sb = new StringBuilder();
        boolean endNotFound = true, notInsideLiteral = true;
        for (int i = start + 2, len = originSql.length(); endNotFound && (i < len); i++) {
            char ch = originSql.charAt(i);
            if (notInsideLiteral && (ch == '>') && (originSql.charAt(i + 1) == '?')) {
                // this is the end of parameter
                endNotFound = false;
            } else {
                sb.append(ch);
                if (this.isDelimiter(ch)) {
                    notInsideLiteral = !notInsideLiteral;
                }
            }
        }
        if (endNotFound) {
            throw new DaoRuntimeException("Invalid parameter, must end with '>?'");
        }
        return sb.toString();
    }

    private boolean isDelimiter(char ch)
    {
        return (ch == '\'') || (ch == '"');
    }
}
